using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Diagnostics;
using EnvDTE;
using EnvDTE80;
using System.CodeDom;
using System.ComponentModel;
using System.IO;
using Microsoft.VisualStudio.Shell.Design;
using Microsoft.VisualStudio.Shell.Interop;
using System.ComponentModel.Design;
using System.CodeDom.Compiler;

namespace Clarius.PresentationModel.Design
{
	[Guid("89E15ED3-420F-479f-8E32-C09529FD81B1")]
	[CustomTool("PresentationModel", "Data binding-friendly presentation model class augmentation code generator for Windows Forms, WPF or Silverlight", true)]
	[ComVisible(true)]
	[VersionSupport("8.0")]
	[VersionSupport("9.0")]
	[VersionSupport("10.0")]
	[CategorySupport(CategorySupportAttribute.CSharpCategory)]
	[CategorySupport(CategorySupportAttribute.VBCategory)]
	public class Tool : CustomTool
	{
		public override string GetDefaultExtension()
		{
			return ".Generated" + base.GetDefaultExtension();
		}

		protected override string OnGenerateCode(string inputFileName, string inputFileContent)
		{
			var undo = OpenUndoContext();
			var edited = false;

			var resolver = GetResolutionService();
			var opt = new System.CodeDom.Compiler.CodeGeneratorOptions();
			opt.BracingStyle = "C";

			try
			{
				var output = new StringWriter();
				var isCSharp = (CurrentProject.Project.Kind == VSLangProj.PrjKind.prjKindCSharpProject);
				var hasImported = false;

				foreach (var cc in FindClasses(CurrentItem.FileCodeModel.CodeElements))
				{
					bool isDerived = IsDerivedFromPresentationModel(cc, resolver);
					bool isSealed = cc.InheritanceKind == vsCMInheritanceKind.vsCMInheritanceKindSealed;
					System.CodeDom.CodeNamespace ns;
					if (cc.Namespace != null)
					{
						ns = new System.CodeDom.CodeNamespace(cc.Namespace.FullName);
						ns.Imports.Add(new CodeNamespaceImport("System"));
						ns.Imports.Add(new CodeNamespaceImport("System.ComponentModel"));
					}
					else
					{
						// import namespaces only once at the top of the file, 
						// as we may be generating multiple classes and it doesn't 
						// work 
						ns = new System.CodeDom.CodeNamespace();
						if (!hasImported)
						{
							hasImported = true;
							ns.Imports.Add(new CodeNamespaceImport("System"));
							ns.Imports.Add(new CodeNamespaceImport("System.ComponentModel"));
						}
					}

					var classNamespace = (cc.Namespace != null ? cc.Namespace.FullName :
						this.CurrentProject.Project.Properties.Item("DefaultNamespace").Value.ToString());
					// get the full class name, works across VB and C#.
					var name = cc.FullName.Replace(classNamespace, "").Substring(1);

					var type = new CodeTypeDeclaration(name);
					SetTypeAccess(cc.Access, type);
					ns.Types.Add(type);

					type.IsPartial = true;
					if (cc.ClassKind != vsCMClassKind.vsCMClassKindPartialClass)
					{
						cc.ClassKind = vsCMClassKind.vsCMClassKindPartialClass;
						edited = true;
					}

					var publicProperties = FindProperties(cc)
						.ForEach(cp =>
						{
							string raiser = "Raise" + cp.Name + "Changed";

							EditPoint2 begin;
							TextPoint end;
							edited |= GetSetterEditPoints(cp, out begin, out end);

							string body = begin.GetText(end);

							if (!body.Contains(raiser))
							{
								CodeExpression raiseCall = new CodeMethodInvokeExpression(
										new CodeThisReferenceExpression(),
										raiser);
								StringWriter sw = new StringWriter();
								GetCodeWriter().GenerateCodeFromExpression(raiseCall, sw, opt);
								if (isCSharp)
									sw.Write(";");
								else
									sw.WriteLine();

								end.CreateEditPoint().Insert(sw.ToString());
								edited = true;
							}

							return;
						})
						.Select(cp => CreateProperty(cp.Name, cp.Type, resolver))
						.ToList();

					new Generator(type, publicProperties, isCSharp, isDerived, isSealed).
						GenerateImplementation();
					GetCodeWriter().GenerateCodeFromNamespace(ns, output, opt);

					// Add the actual validation method to the main class.
					//protected virtual|override void DoValidate()
					if (AddFunction(cc, isCSharp, "DoValidate", isDerived, "TODO: DataBinding - Implement instance validation here"))
					{
						edited = true;
					}

					// Add the initialization method to the main class.
					//protected virtual|override void DoInitialize()
					if (AddFunction(cc, isCSharp, "DoInitialize", isDerived, "TODO: DataBinding - Implement initialization here"))
					{
						edited = true;
					}
				}

				if (edited)
				{
					try
					{
						CurrentItem.DTE.ExecuteCommand("Edit.FormatDocument", "");
					}
					catch
					{
					}
				}

				return output.ToString();
			}
			catch
			{
				base.CurrentProject.DTE.UndoContext.SetAborted();
				throw;
			}
			finally
			{
				if (undo && base.CurrentProject.DTE.UndoContext.IsOpen)
				{
					base.CurrentProject.DTE.UndoContext.Close();
				}
			}
		}

		private static bool GetSetterEditPoints(CodeProperty cp, out EditPoint2 begin, out TextPoint end)
		{
			try
			{
				// Regular C# or VB properties.
				begin = (EditPoint2)cp.Setter.GetStartPoint(vsCMPart.vsCMPartBody).CreateEditPoint();
				end = cp.Setter.GetEndPoint(vsCMPart.vsCMPartBody);

				return false;
			}
			catch (COMException)
			{
				// If we have C# 3.5 automatic properties, 
				// we need to change them to regular properties.
				begin = (EditPoint2)cp.Setter.StartPoint.CreateEditPoint();
				end = cp.Setter.EndPoint;

				if (begin.GetText(end) == "set;")
				{
					// add backing field.
					string fieldName = "_" + Char.ToLower(cp.Name[0]) + cp.Name.Substring(1);
					cp.Parent.AddVariable(fieldName, cp.Type, 0, vsCMAccess.vsCMAccessDefault, null);

					// TODO: we know this is a C# 3.5 property. I guess 
					// it's useless to try to use CodeDom here?

					// replace getter

					begin = (EditPoint2)cp.Getter.StartPoint.CreateEditPoint();
					end = cp.Getter.EndPoint;
					begin.Delete(end);
					begin.CreateEditPoint().Insert("\r\nget { return " + fieldName + "; }");

					// replace setter
					begin = (EditPoint2)cp.Setter.StartPoint.CreateEditPoint();
					end = cp.Setter.EndPoint;
					begin.Delete(end);
					begin.CreateEditPoint().Insert("\r\nset { " + fieldName + " = value; }\r\n");

					// we now have a regular C# property. 
					GetSetterEditPoints(cp, out begin, out end);

					return true;
				}
				else
				{
					throw;
				}
			}
		}

		private void SetTypeAccess(vsCMAccess access, CodeTypeDeclaration type)
		{
			switch (access)
			{
				case vsCMAccess.vsCMAccessAssemblyOrFamily:
				case vsCMAccess.vsCMAccessProjectOrProtected:
					type.Attributes = MemberAttributes.FamilyOrAssembly;
					type.TypeAttributes = System.Reflection.TypeAttributes.NotPublic;
					break;

				case vsCMAccess.vsCMAccessProject:
					type.Attributes = MemberAttributes.Assembly;
					type.TypeAttributes = System.Reflection.TypeAttributes.NotPublic;
					break;

				case vsCMAccess.vsCMAccessPublic:
					type.Attributes = MemberAttributes.Public;
					type.TypeAttributes = System.Reflection.TypeAttributes.Public;
					break;

				case vsCMAccess.vsCMAccessPrivate:
				case vsCMAccess.vsCMAccessProtected:
				case vsCMAccess.vsCMAccessWithEvents:
				case vsCMAccess.vsCMAccessDefault:
				default:
					throw new NotSupportedException("Unsupported class access " + access);
			}

		}

		private bool AddFunction(CodeClass2 cc, bool isCSharp, string name, bool isDerived, string comment)
		{
			CodeFunction2 function = FindMethod(cc.Children, name);
			if (function == null)
			{
				try
				{
					function = (CodeFunction2)cc.AddFunction(name,
						vsCMFunction.vsCMFunctionSub,
						vsCMTypeRef.vsCMTypeRefVoid,
						cc.Children.Count,
						vsCMAccess.vsCMAccessProtected, null);
				}
				catch
				{
					// Retry adding at one less the count of children.
					// Workaround for VB.
					function = (CodeFunction2)cc.AddFunction(name,
						vsCMFunction.vsCMFunctionSub,
						vsCMTypeRef.vsCMTypeRefVoid,
						cc.Children.Count - 1,
						vsCMAccess.vsCMAccessProtected, null);
				}

				if (isDerived)
				{
					// override base
					function.OverrideKind = vsCMOverrideKind.vsCMOverrideKindOverride;

					// call base
					CodeMethodInvokeExpression invoke = new CodeMethodInvokeExpression(
						new CodeBaseReferenceExpression(), name);
					CodeGeneratorOptions opt = new CodeGeneratorOptions();
					opt.BracingStyle = "C";

					StringWriter sw = new StringWriter();
					GetCodeWriter().GenerateCodeFromExpression(invoke, sw, opt);
					if (isCSharp)
						sw.WriteLine(";");
					else
						sw.WriteLine();

					TextPoint point = function.GetStartPoint(vsCMPart.vsCMPartBody);
					point.CreateEditPoint().Insert(sw.ToString());
				}
				else
				{
					function.CanOverride = true;
				}

				// TODO: throws?
				//doValidate.DocComment = "Validates the instance state.";
				function.Comment = comment;

				return true;
			}
			else
			{
				// Update the method definition and add the call to the base
				// if the method exists but it's not an override.
				if (isDerived && !(function.OverrideKind == vsCMOverrideKind.vsCMOverrideKindOverride))
				{
					// override base
					function.OverrideKind = vsCMOverrideKind.vsCMOverrideKindOverride;

					// call base
					CodeMethodInvokeExpression invoke = new CodeMethodInvokeExpression(
						new CodeBaseReferenceExpression(), name);
					CodeGeneratorOptions opt = new CodeGeneratorOptions();
					opt.BracingStyle = "C";

					StringWriter sw = new StringWriter();
					GetCodeWriter().GenerateCodeFromExpression(invoke, sw, opt);
					if (isCSharp)
						sw.WriteLine(";");
					else
						sw.WriteLine();

					TextPoint point = function.GetStartPoint(vsCMPart.vsCMPartBody);
					point.CreateEditPoint().Insert(sw.ToString());
				}
			}

			return false;
		}

		private Property CreateProperty(string name, CodeTypeRef type, ITypeResolutionService resolver)
		{
			bool shouldCast, supportsInitialize, supportsInitializeNotification, supportsChangeTracking;
			if (type.CodeType == null || !(type.CodeType is CodeClass2))
			{
				// VB doesn't load CodeType for types not defined in a project, 
				// unlike C# which is more consistent. But C# has the problem 
				// that it exposes the CodeType but the get_IsDerivedFrom doesn't work 
				// with the partial classes associated with the type, so we need to 
				// go with the resolver anyway.
				Type t = GetResolvedType(type.AsFullName, resolver);
				shouldCast = !(t.IsValueType || t == typeof(string));
				supportsInitialize = typeof(ISupportInitialize).IsAssignableFrom(t);
				supportsInitializeNotification = typeof(ISupportInitializeNotification).IsAssignableFrom(t);
				supportsChangeTracking = typeof(IChangeTracking).IsAssignableFrom(t);
			}
			else
			{
				CodeClass2 cc = (CodeClass2)type.CodeType;

				shouldCast = !(IsDerivedFrom(cc, typeof(ValueType)) || type.AsFullName == "System.String");
				supportsInitialize = IsDerivedFrom(cc, typeof(ISupportInitialize));
				supportsInitializeNotification = IsDerivedFrom(cc, typeof(ISupportInitializeNotification));
				supportsChangeTracking = IsDerivedFrom(cc, typeof(IChangeTracking));
			}

			return new Property(name, shouldCast,
				supportsInitialize,
				supportsInitializeNotification,
				supportsChangeTracking);
		}

		private bool IsDerivedFrom(CodeClass2 cc, Type type)
		{
			if (IsDerivedOrImplements(cc, type))
			{
				return true;
			}
			if (cc.ClassKind == vsCMClassKind.vsCMClassKindPartialClass)
			{
				foreach (CodeClass2 pc in cc.PartialClasses)
				{
					if (IsDerivedOrImplements(pc, type))
					{
						return true;
					}
				}
			}

			return false;
		}

		private static bool IsDerivedOrImplements(CodeClass2 cc, Type type)
		{
			if (cc.get_IsDerivedFrom(type.FullName))
			{
				return true;
			}

			foreach (CodeElement iface in cc.ImplementedInterfaces)
			{
				if (iface.FullName == type.FullName)
				{
					return true;
				}
			}

			return false;
		}

		private bool IsDerivedFromPresentationModel(CodeClass2 cc, ITypeResolutionService resolver)
		{
			foreach (CodeElement el in cc.Bases)
			{
				CodeClass2 baseClass = el as CodeClass2;
				if (baseClass != null)
				{
					if (baseClass.InfoLocation == vsCMInfoLocation.vsCMInfoLocationProject)
					{
						if (IsPresentationModel(baseClass))
						{
							return true;
						}
						else
						{
							if (IsDerivedFromPresentationModel(baseClass, resolver))
							{
								return true;
							}
						}
					}
					else
					{
						Type baseType = GetResolvedType(baseClass.FullName, resolver);
						if (IsPresentationModel(baseType))
						{
							return true;
						}
					}
				}
			}

			return false;
		}

		private bool IsPresentationModel(CodeClass2 baseClass)
		{
			return baseClass.PartialClasses
				.OfType<CodeClass2>()
				.Select(cc => cc.Attributes)
				.SelectMany(ce => ce.OfType<CodeAttribute2>())
				.Concat(baseClass.Attributes.OfType<CodeAttribute2>())
				.Where(attr =>
					attr.FullName == typeof(GeneratedCodeAttribute).FullName &&
					attr.Value.Contains(ThisAssembly.Title))
				.Count() != 0;
		}

		private bool IsPresentationModel(Type baseType)
		{
			GeneratedCodeAttribute gc = (GeneratedCodeAttribute)Attribute.GetCustomAttribute(
				baseType, typeof(GeneratedCodeAttribute));

			return gc != null && gc.Tool == ThisAssembly.Title;
		}

		private ITypeResolutionService GetResolutionService()
		{
			DynamicTypeService typeService = GetService<DynamicTypeService>();
			Debug.Assert(typeService != null, "No dynamic type service registered.");

			IVsSolution sln = GetService<IVsSolution>();
			IVsHierarchy hier;
			sln.GetProjectOfUniqueName(CurrentProject.Project.UniqueName, out hier);

			Debug.Assert(hier != null, "No active hierarchy is selected.");

			ITypeResolutionService resolver = typeService.GetTypeResolutionService(hier);
			//ITypeDiscoveryService discovery = typeService.GetTypeDiscoveryService(hier);
			return resolver;
		}

		private bool OpenUndoContext()
		{
			// If necessary, create a new undo context
			if (!base.CurrentProject.DTE.UndoContext.IsOpen)
			{
				base.CurrentProject.DTE.UndoContext.Open(
					String.Format(
						Properties.Resources.UndoLabel,
						base.CurrentItem.Name),
					false);

				return true;
			}
			else
			{
				return false;
			}
		}

		private CodeFunction2 FindMethod(CodeElements elements, string methodName)
		{
			return elements
				.OfType<CodeFunction2>()
				.Where(func => func.Name == methodName)
				.FirstOrDefault();
		}

		private IEnumerable<CodeProperty> FindProperties(CodeClass2 cc)
		{
			return cc.Children
				.OfType<CodeProperty>()
				.Where(prop =>
					prop.Access == vsCMAccess.vsCMAccessPublic &&
					prop.Setter != null &&
					prop.Setter.Access == vsCMAccess.vsCMAccessPublic &&
					prop.Getter != null &&
					prop.Getter.Access == vsCMAccess.vsCMAccessPublic);
		}

		private IEnumerable<CodeClass2> FindClasses(CodeElements elements)
		{
			foreach (CodeElement el in elements)
			{
				CodeClass2 cc = el as CodeClass2;
				EnvDTE.CodeNamespace ns = el as EnvDTE.CodeNamespace;
				if (cc != null) yield return cc;
				if (ns != null)
				{
					foreach (CodeClass2 inner in FindClasses(ns.Children))
					{
						yield return inner;
					}
				}
			}

		}

		private Type GetResolvedType(string typeName, ITypeResolutionService resolver)
		{
			// Fix generic type names. i.e.:
			// "System.Collections.Generic.IList<SimpleSharing.Item>"
			// should be:
			// "System.Collections.Generic.IList`1[[SimpleSharing.Item]]"

			// TODO: add support for nested generics.
			// this is very simple probing.
			int typeparams = typeName.ToCharArray().Where(c => c == '<').Count();

			if (typeparams > 0)
			{
				typeName = typeName.Replace("<", "`" + typeparams + "[[").Replace(">", "]]");
			}

			return resolver.GetType(typeName, true);
		}

		#region Registration and Installation

		/// <summary>
		/// Registers the generator.
		/// </summary>
		[ComRegisterFunction]
		public static void RegisterClass(Type type)
		{
			CustomTool.Register(typeof(Tool));
		}

		/// <summary>
		/// Unregisters the generator.
		/// </summary>
		[ComUnregisterFunction]
		public static void UnregisterClass(Type t)
		{
			CustomTool.UnRegister(typeof(Tool));
		}

		#endregion Registration and Installation

		#region Unused

		//private string CompileProject()
		//{
		//   // Force compilation of the current project. We need the type in the output.
		//   CurrentItem.DTE.Solution.SolutionBuild.BuildProject(
		//      CurrentItem.DTE.Solution.SolutionBuild.ActiveConfiguration.Name,
		//      CurrentItem.ContainingProject.UniqueName, true);

		//   if (CurrentItem.DTE.Solution.SolutionBuild.LastBuildInfo == 1)
		//   {
		//      // If it does not compile, return the same code we had, if any, 
		//      // to avoid further breaks.
		//      string file = (string)CurrentItem.Properties.Item("FullPath").Value;
		//      file = Path.ChangeExtension(file, GetDefaultExtension());
		//      // Either the current file is already our output, or it's the parent file 
		//      // and we change the extension accordingly.
		//      if (File.Exists(file))
		//      {
		//         return File.ReadAllText(file);
		//      }
		//   }
		//}

		// Auto-format so that we get the built-in format regardless of how we 
		// added stuff.
		//if (edited)
		//{
		// Leaves entire document selected. Ignore for now.
		//TextDocument doc = (TextDocument)base.CurrentItem.Document.Object("TextDocument");
		//if (doc != null)
		//{
		//   int line = doc.Selection.ActivePoint.Line;
		//   int charoff = doc.Selection.ActivePoint.LineCharOffset;

		//   doc.Selection.SelectAll();
		//   doc.Selection.SmartFormat();
		//   base.CurrentProject.DTE.ExecuteCommand("Edit.FormatSelection", null);
		//   doc.Selection.MoveToLineAndOffset(line, charoff, false);
		//}
		//}

		#endregion
	}
}
